package rtsp_client

import (
	"bufio"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"net"
	"net/http"
	"net/url"
	"strconv"
	"strings"
)

// Client describes an RTSP Client.
type Client struct {
	cSeq      int
	addr      string
	url       *url.URL
	conn      net.Conn
	sessionID string
}

// NewClient returns a pointer to a new Client and the local address of the
// RTSP connection. The address addr will be parsed and a connection to the
// RTSP server will be made.
func NewClient(addr string) (c *Client, local, remote *net.TCPAddr, err error) {
	c = &Client{addr: addr}
	c.url, err = url.Parse(addr)
	if err != nil {
		return nil, nil, nil, err
	}
	c.conn, err = net.Dial("tcp", c.url.Host)
	if err != nil {
		return nil, nil, nil, err
	}
	local = c.conn.LocalAddr().(*net.TCPAddr)
	remote = c.conn.RemoteAddr().(*net.TCPAddr)
	return
}

// Close closes the RTSP connection.
func (c *Client) Close() error {
	return c.conn.Close()
}

// Describe forms and sends an RTSP request of method DESCRIBE to the RTSP server.
func (c *Client) Describe() (*Response, error) {
	req, err := NewRequest("DESCRIBE", c.nextCSeq(), c.url, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Add("Accept", "application/sdp")
	return c.Do(req)
}

// Options forms and sends an RTSP request of method OPTIONS to the RTSP server.
func (c *Client) Options() (*Response, error) {
	req, err := NewRequest("OPTIONS", c.nextCSeq(), c.url, nil)
	if err != nil {
		return nil, err
	}
	return c.Do(req)
}

// Setup forms and sends an RTSP request of method SETUP to the RTSP server.
func (c *Client) Setup(track, transport string) (*Response, error) {
	u, err := url.Parse(c.addr + "/" + track)
	if err != nil {
		return nil, err
	}

	req, err := NewRequest("SETUP", c.nextCSeq(), u, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Add("Transport", transport)

	resp, err := c.Do(req)
	if err != nil {
		return nil, err
	}
	c.sessionID = resp.Header.Get("Session")

	return resp, err
}

// Play forms and sends an RTSP request of method PLAY to the RTSP server
func (c *Client) Play() (*Response, error) {
	req, err := NewRequest("PLAY", c.nextCSeq(), c.url, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Add("Session", c.sessionID)

	return c.Do(req)
}

// Do sends the given RTSP request req, reads any responses and returns the response
// and any errors.
func (c *Client) Do(req *Request) (*Response, error) {
	err := req.Write(c.conn)
	if err != nil {
		return nil, err
	}

	resp, err := ReadResponse(c.conn)
	if err != nil {
		return nil, err
	}

	return resp, nil
}

// nextCSeq provides the next CSeq number for the next RTSP request.
func (c *Client) nextCSeq() string {
	c.cSeq++
	return strconv.Itoa(c.cSeq)
}

onst minResponse = 12

var errInvalidResponse = errors.New("invalid response")

// Request describes an RTSP request.
type Request struct {
	Method        string
	URL           *url.URL
	Proto         string
	ProtoMajor    int
	ProtoMinor    int
	Header        http.Header
	ContentLength int
	Body          io.ReadCloser
}

// NewRequest returns a pointer to a new Request.
func NewRequest(method, cSeq string, u *url.URL, body io.ReadCloser) (*Request, error) {
	req := &Request{
		Method:     method,
		URL:        u,
		Proto:      "RTSP",
		ProtoMajor: 1,
		ProtoMinor: 0,
		Header:     map[string][]string{"CSeq": []string{cSeq}},
		Body:       body,
	}
	return req, nil
}

// Write writes the request r to the given io.Writer w.
func (r *Request) Write(w io.Writer) error {
	_, err := w.Write([]byte(r.String()))
	return err
}

// String returns a formatted string of the Request.
func (r Request) String() string {
	var b strings.Builder
	fmt.Fprintf(&b, "%s %s %s/%d.%d\r\n", r.Method, r.URL.String(), r.Proto, r.ProtoMajor, r.ProtoMinor)
	for k, v := range r.Header {
		for _, v := range v {
			fmt.Fprintf(&b, "%s: %s\r\n", k, v)
		}
	}
	b.WriteString("\r\n")
	if r.Body != nil {
		s, _ := ioutil.ReadAll(r.Body)
		b.WriteString(string(s))
	}
	return b.String()
}

// Response describes an RTSP response.
type Response struct {
	Proto         string
	ProtoMajor    int
	ProtoMinor    int
	StatusCode    int
	ContentLength int
	Header        http.Header
	Body          io.ReadCloser
}

// String returns a formatted string of the Response.
func (r Response) String() string {
	var b strings.Builder
	fmt.Fprintf(&b, "%s/%d.%d %d\n", r.Proto, r.ProtoMajor, r.ProtoMinor, r.StatusCode)
	for k, v := range r.Header {
		for _, v := range v {
			fmt.Fprintf(&b, "%s: %s", k, v)
		}
	}
	return b.String()
}

// ReadResponse will read the response of the RTSP request from the connection,
// and return a pointer to a new Response.
func ReadResponse(r io.Reader) (*Response, error) {
	resp := &Response{Header: make(map[string][]string)}

	scanner := bufio.NewScanner(r)

	// Read the first line.
	scanner.Scan()
	err := scanner.Err()
	if err != nil {
		return nil, err
	}
	s := scanner.Text()

	if len(s) < minResponse || !strings.HasPrefix(s, "RTSP/") {
		return nil, errInvalidResponse
	}
	resp.Proto = "RTSP"

	n, err := fmt.Sscanf(s[5:], "%d.%d %d", &resp.ProtoMajor, &resp.ProtoMinor, &resp.StatusCode)
	if err != nil || n != 3 {
		return nil, fmt.Errorf("could not Sscanf response, error: %w", err)
	}

	// Read headers.
	for scanner.Scan() {
		err = scanner.Err()
		if err != nil {
			return nil, err
		}
		parts := strings.SplitN(scanner.Text(), ":", 2)
		if len(parts) < 2 {
			break
		}
		resp.Header.Add(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]))
	}
	// Get the content length from the header.
	resp.ContentLength, _ = strconv.Atoi(resp.Header.Get("Content-Length"))

	resp.Body = closer{r}
	return resp, nil
}

type closer struct {
	io.Reader
}

func (c closer) Close() error {
	if c.Reader == nil {
		return nil
	}
	defer func() {
		c.Reader = nil
	}()
	if r, ok := c.Reader.(io.ReadCloser); ok {
		return r.Close()
	}
	return nil
}